/*
 * @(#)Game.java
 *
 * This work is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of
 * the License, or (at your option) any later version.
 *
 * This work is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * Copyright (c) 2003 Per Cederberg. All rights reserved.
 */

package tetris;

import java.awt.Button;
import java.awt.Checkbox;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Label;
import java.awt.Rectangle;
import java.awt.TextField;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.Random;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

/**
 * The Tetris game. This class controls all events in the game and
 * handles all the game logics. The game is started through user
 * interaction with the graphical game component provided by this 
 * class.
 *
 * @version  1.2
 * @author   Per Cederberg, per@percederberg.net
 */
public class Game extends Object {
    
    private BlockingQueue<Double> scoreQueue = new ArrayBlockingQueue<Double>(1);
    private long seed;
    
    public void newSeed(){
        this.seed = 0;
    }
    
    public void setAi(TetrisAI ai) {
        this.ai = ai;
    }
    
    public TetrisAI getAi(){
        return ai;
    }
    
    public double run(){
        handleStart();
        try {
            return scoreQueue.take().doubleValue();
        } catch (InterruptedException ex) {
            System.out.println("Interrupted!");
            return 0.0;
        }
    }
    
    TetrisAI ai = new StupidAI();
    public static Random rnd = new Random(1);
    /**
     * The main square board. This board is used for the game itself.
     */
    private SquareBoard board = null;

    /**
     * The preview square board. This board is used to display a 
     * preview of the figures.
     */
    private SquareBoard previewBoard = new SquareBoard(5, 5);

    /**
     * The figures used on both boards. All figures are reutilized in 
     * order to avoid creating new objects while the game is running.
     * Special care has to be taken when the preview figure and the
     * current figure refers to the same object.
     */
    private Figure[] figures = {
        new Figure(Figure.SQUARE_FIGURE),
        new Figure(Figure.LINE_FIGURE),
        new Figure(Figure.S_FIGURE),
        new Figure(Figure.Z_FIGURE),
        new Figure(Figure.RIGHT_ANGLE_FIGURE),
        new Figure(Figure.LEFT_ANGLE_FIGURE),
        new Figure(Figure.TRIANGLE_FIGURE)
    };

    /**
     * The graphical game component. This component is created on the
     * first call to getComponent().
     */
    private GamePanel component = null;

    /**
     * The thread that runs the game. When this variable is set to 
     * null, the game thread will terminate.
     */
    private GameThread thread = null;

    /**
     * The game level. The level will be increased for every 20 lines 
     * removed from the square board.
     */
    private int level = 1;

    /**
     * The current score. The score is increased for every figure that
     * is possible to place on the main board.
     */
    private int score = 0;

    /**
     * The current figure. The figure will be updated when 
     */
    private Figure figure = null;

    /**
     * The next figure.
     */
    private Figure nextFigure = null;
    
    /**
     * The rotation of the next figure.
     */
    private int nextRotation = 0;

    /**
     * The figure preview flag. If this flag is set, the figure
     * will be shown in the figure preview board.
     */
    private boolean preview = true;

    /**
     * The move lock flag. If this flag is set, the current figure
     * cannot be moved. This flag is set when a figure is moved all 
     * the way down, and reset when a new figure is displayed.
     */
    private boolean moveLock = false;

    /**
     * Creates a new Tetris game. The square board will be given
     * the default size of 10x20.
     */
    public Game() {
        this(10, 20);
    }

    /**
     * Creates a new Tetris game. The square board will be given
     * the specified size.
     *
     * @param width     the width of the square board (in positions)
     * @param height    the height of the square board (in positions)
     */
    public Game(int width, int height) {
        board = new SquareBoard(width, height);
        board.setMessage("Press start");
        thread = new GameThread();
    }

    /**
     * Kills the game running thread and makes necessary clean-up.
     * After calling this method, no further methods in this class
     * should be called. Neither should the component returned
     * earlier be trusted upon.
     */
    public void quit() {
        thread = null;
    }

    /**
     * Returns a new component that draws the game.
     * 
     * @return the component that draws the game
     */
    public Component getComponent() {
        if (component == null) {
            component = new GamePanel();
        }
        return component;
    }

    /**
     * Handles a game start event. Both the main and preview square
     * boards will be reset, and all other game parameters will be
     * reset. Finally the game thread will be launched.
     */
    boolean fullSpeed = true;
    boolean aiPlay = true;
    private void handleStart() {
        try {
            seed = Integer.parseInt(component.seed.getText());
        } catch (NumberFormatException ex) {
        }
        
        if (seed == 0) {
            seed = System.currentTimeMillis();
            component.seed.setText("" + seed);
        }
        rnd = new Random(seed);
        aiPlay = component.ai.getState();
        fullSpeed = component.fullSpeed.getState();
        // Reset score and figures
        level = 1;
        score = 0;
        figure = null;
        nextFigure = randomFigure();
        nextFigure.rotateRandom();
        nextRotation = nextFigure.getRotation();  

        // Reset components
        board.setMessage(null);
        board.clear();
        previewBoard.clear();
        handleLevelModification();
        handleScoreModification();
        component.button.setLabel("Pause");

        // Start game thread
        
        thread.reset();
    }

    /**
     * Handles a game over event. This will stop the game thread,
     * reset all figures and print a game over message.
     */
    private void handleGameOver() {

        // Stop game thred
        thread.setPaused(true);

        // Reset figures
        if (figure != null) {
            figure.detach();
        }
        figure = null;
        if (nextFigure != null) {
            nextFigure.detach();
        }
        nextFigure = null;

        // Handle components
        board.setMessage("Game Over");
        component.button.setLabel("Start");
        try {
            scoreQueue.put(new Double((double) score));
        } catch (InterruptedException ex) {
            System.out.println("Interrupted!");
        }
    }

    /**
     * Handles a game pause event. This will pause the game thread and
     * print a pause message on the game board.
     */
    private void handlePause() {
        thread.setPaused(true);
        board.setMessage("Paused");
        component.button.setLabel("Resume");
    }

    /**
     * Handles a game resume event. This will resume the game thread 
     * and remove any messages on the game board.
     */
    private void handleResume() {
        board.setMessage(null);
        component.button.setLabel("Pause");
        thread.setPaused(false);
    }

    /**
     * Handles a level modification event. This will modify the level
     * label and adjust the thread speed.
     */
    private void handleLevelModification() {
        component.levelLabel.setText("Level: " + level);
        thread.adjustSpeed();
    }
    
    /**
     * Handle a score modification event. This will modify the score
     * label.
     */
    private void handleScoreModification() {
        component.scoreLabel.setText("Score: " + score);
    }

    /**
     * Handles a figure start event. This will move the next figure
     * to the current figure position, while also creating a new 
     * preview figure. If the figure cannot be introduced onto the
     * game board, a game over event will be launched.
     */
    private void handleFigureStart() {
        int  rotation;

        // Move next figure to current
        figure = nextFigure;
        moveLock = false;
        rotation = nextRotation;
        nextFigure = randomFigure();
        nextFigure.rotateRandom(); 
        nextRotation = nextFigure.getRotation(); 

        // Handle figure preview
        if (preview) {
            previewBoard.clear(); 
            nextFigure.attach(previewBoard, true);
            nextFigure.detach();
        }

        // Attach figure to game board
        figure.setRotation(rotation);
        if (!figure.attach(board, false)) {
            previewBoard.clear();
            figure.attach(previewBoard, true);
            figure.detach();
            handleGameOver();
        }
        if(aiPlay)
            ai.makeBestMove(board.getBoard(), figure);
    }
    
    /**
     * Handles a figure landed event. This will check that the figure
     * is completely visible, or a game over event will be launched.
     * After this control, any full lines will be removed. If no full
     * lines could be removed, a figure start event is launched 
     * directly.
     */
    private void handleFigureLanded() {
        // Check and detach figure
        if (figure.isAllVisible()) {
            score += 10;
            handleScoreModification();
           /* System.out.print("" + ((SlightlyLessStupidAI)ai).contour(board.getBoard())[0] + ", ");
            System.out.print("" + ((SlightlyLessStupidAI)ai).contour(board.getBoard())[1] + ", ");
            System.out.print("" + ((SlightlyLessStupidAI)ai).contour(board.getBoard())[2] + ", ");
            System.out.print("" + ((SlightlyLessStupidAI)ai).contour(board.getBoard())[3] + ", ");
            System.out.print("" + ((SlightlyLessStupidAI)ai).contour(board.getBoard())[4] + ", ");
            System.out.print("" + ((SlightlyLessStupidAI)ai).contour(board.getBoard())[5] + ", ");
            System.out.print("" + ((SlightlyLessStupidAI)ai).contour(board.getBoard())[6] + ", ");
            System.out.print("" + ((SlightlyLessStupidAI)ai).contour(board.getBoard())[7] + ", ");
            System.out.print("" + ((SlightlyLessStupidAI)ai).contour(board.getBoard())[8] + ", ");
            System.out.println("" + ((SlightlyLessStupidAI)ai).contour(board.getBoard())[9]);
            int[] t = ((SlightlyLessStupidAI)ai).contour(board.getBoard());
            System.out.println("bumbs "+((SlightlyLessStupidAI)ai).bumpiness(t));
            System.out.println("chasms "+((SlightlyLessStupidAI)ai).chasms(t));
            System.out.println("full "+((SlightlyLessStupidAI)ai).getFullLines(board.getBoard()));*/
        } else {
            handleGameOver();
            return;
        }
        figure.detach();
        figure = null;
        // Check for full lines or create new figure
        if (board.hasFullLines()) {
            board.removeFullLines();
            if (level < 9 && board.getRemovedLines() / 20 > level) {
                level = board.getRemovedLines() / 20;
                handleLevelModification();
            }
        } else {
            handleFigureStart();
        }
    }

    /**
     * Handles a timer event. This will normally move the figure down
     * one step, but when a figure has landed or isn't ready other 
     * events will be launched. This method is synchronized to avoid 
     * race conditions with other asynchronous events (keyboard and
     * mouse).
     */
    private synchronized void handleTimer() {
        if (figure == null) {
            handleFigureStart();
        } else if (figure.hasLanded()) {
            handleFigureLanded();
        } else {
            figure.moveDown();
        }
    }

    /**
     * Handles a button press event. This will launch different events
     * depending on the state of the game, as the button semantics
     * change as the game changes. This method is synchronized to 
     * avoid race conditions with other asynchronous events (timer and
     * keyboard).
     */
    private synchronized void handleButtonPressed() {
        if (nextFigure == null) {
            handleStart();
        } else if (thread.isPaused()) {
            handleResume();
        } else {
            handlePause();
        }
    }

    /**
     * Handles a keyboard event. This will result in different actions
     * being taken, depending on the key pressed. In some cases, other
     * events will be launched. This method is synchronized to avoid 
     * race conditions with other asynchronous events (timer and 
     * mouse).
     * 
     * @param e         the key event
     */
    private synchronized void handleKeyEvent(KeyEvent e) {

        // Handle start, pause and resume
        if (e.getKeyCode() == KeyEvent.VK_P) {
            handleButtonPressed();
            return;
        }

        // Don't proceed if stopped or paused
        if (figure == null || moveLock || thread.isPaused()) {
            return;
        }

        // Handle remaining key events
        switch (e.getKeyCode()) {

        case KeyEvent.VK_LEFT:
            figure.moveLeft();
            break;

        case KeyEvent.VK_RIGHT:
            figure.moveRight();
            break;

        case KeyEvent.VK_DOWN:
            figure.moveAllWayDown();
            moveLock = true;
            break;

        case KeyEvent.VK_UP:
        case KeyEvent.VK_SPACE:
            if (e.isControlDown()) {
                figure.rotateRandom();  
            } else if (e.isShiftDown()) { 
                figure.rotateClockwise();
            } else {
                figure.rotateCounterClockwise();
            }
            break;

        case KeyEvent.VK_S:
            if (level < 9) {
                level++;
                handleLevelModification();
            }
            break;

        case KeyEvent.VK_N:
            preview = !preview;
            if (preview && figure != nextFigure) {
                nextFigure.attach(previewBoard, true);
                nextFigure.detach(); 
            } else {
                previewBoard.clear();
            }
            break;
        }
    }

    /**
     * Returns a random figure. The figures come from the figures
     * array, and will not be initialized.
     * 
     * @return a random figure
     */
    
    private Figure randomFigure() {
        return figures[(int) (rnd.nextDouble() * figures.length)];
    }


    /**
     * The game time thread. This thread makes sure that the timer
     * events are launched appropriately, making the current figure 
     * fall. This thread can be reused across games, but should be set
     * to paused state when no game is running.
     */
    private class GameThread extends Thread {

        /**
         * The game pause flag. This flag is set to true while the 
         * game should pause.
         */
        private boolean paused = true;

        /**
         * The number of milliseconds to sleep before each automatic
         * move. This number will be lowered as the game progresses.
         */
        private int sleepTime = 500;

        /**
         * Creates a new game thread with default values.
         */
        public GameThread() {
        }

        /**
         * Resets the game thread. This will adjust the speed and 
         * start the game thread if not previously started.
         */
        public void reset() {
            adjustSpeed();
            setPaused(false);
            if (!isAlive()) {
                this.start();
            }
        }

        /**
         * Checks if the thread is paused.
         * 
         * @return true if the thread is paused, or
         *         false otherwise
         */
        public boolean isPaused() {
            return paused;
        }

        /**
         * Sets the thread pause flag.
         * 
         * @param paused     the new paused flag value
         */
        public void setPaused(boolean paused) {
            this.paused = paused;
        }

        /**
         * Adjusts the game speed according to the current level. The 
         * sleeping time is calculated with a function making larger 
         * steps initially an smaller as the level increases. A level 
         * above ten (10) doesn't have any further effect.
         */
        public void adjustSpeed() {
            sleepTime = 4500 / (level + 5) - 250;
            if (sleepTime < 50) {
                sleepTime = 50;
            }
        }

        /**
         * Runs the game.
         */
        public void run() {
            while (thread == this) {
                // Make the time step
                handleTimer();
                                
                if(!fullSpeed){
                    // Sleep for some time
                    try {
                        Thread.sleep(sleepTime);
                    } catch (InterruptedException ignore) {
                        // Do nothing
                    }//*/
                }

                // Sleep if paused
                while (paused && thread == this) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException ignore) {
                         //Do nothing
                    }
                }
            }
        }
    }


    /**
     * A game panel component. Contains all the game components.
     */
    private class GamePanel extends Container {
        
        /**
         * The component size. If the component has been resized, that 
         * will be detected when the paint method executes. If this 
         * value is set to null, the component dimensions are unknown.
         */
        private Dimension  size = null;

        /**
         * The score label.
         */
        private Label scoreLabel = new Label("Score: 0");

        /**
         * The level label.
         */
        private Label levelLabel = new Label("Level: 1");

        /**
         * The generic button.
         */
        private Button button = new Button("Start");
        public TextField seed = new TextField("1");
        public Checkbox ai = new Checkbox("AI play", true) ;
        public Checkbox fullSpeed = new Checkbox("Full speed", true) ;
        /**
         * Creates a new game panel. All the components will
         * also be added to the panel.
         */
        public GamePanel() {
            super();
            initComponents();
        }

        /**
         * Paints the game component. This method is overridden from 
         * the default implementation in order to set the correct 
         * background color.
         * 
         * @param g     the graphics context to use
         */
        public void paint(Graphics g) {
            Rectangle  rect = g.getClipBounds();

            if (size == null || !size.equals(getSize())) {
                size = getSize();
                resizeComponents();
            }
            g.setColor(getBackground());
            g.fillRect(rect.x, rect.y, rect.width, rect.height);
            super.paint(g);
        }
        
        /**
         * Initializes all the components, and places them in
         * the panel.
         */
        private void initComponents() {
            GridBagConstraints  c;

            // Set layout manager and background
            setLayout(new GridBagLayout());
            setBackground(Configuration.getColor("background", "#d4d0c8"));

            // Add game board
            c = new GridBagConstraints();
            c.gridx = 0;
            c.gridy = 0;
            c.gridheight = 8;
            c.weightx = 1.0;
            c.weighty = 1.0;
            c.fill = GridBagConstraints.BOTH;
            this.add(board.getComponent(), c);

            // Add next figure board
            c = new GridBagConstraints();
            c.gridx = 1;
            c.gridy = 0;
            c.weightx = 0.2;
            c.weighty = 0.18;
            c.fill = GridBagConstraints.BOTH;
            c.insets = new Insets(15, 15, 0, 15);
            this.add(previewBoard.getComponent(), c);

            // Add score label
            scoreLabel.setForeground(Configuration.getColor("label", 
                                                            "#000000"));
            scoreLabel.setAlignment(Label.CENTER);
            c = new GridBagConstraints();
            c.gridx = 1;
            c.gridy = 1;
            c.weightx = 0.3;
            c.weighty = 0.05;
            c.anchor = GridBagConstraints.CENTER;
            c.fill = GridBagConstraints.BOTH;
            c.insets = new Insets(0, 15, 0, 15);
            this.add(scoreLabel, c);
            
            


            // Add level label
            levelLabel.setForeground(Configuration.getColor("label", 
                                                            "#000000"));
            levelLabel.setAlignment(Label.CENTER);
            c = new GridBagConstraints();
            c.gridx = 1;
            c.gridy = 2;
            c.weightx = 0.3;
            c.weighty = 0.05;
            c.anchor = GridBagConstraints.CENTER;
            c.fill = GridBagConstraints.BOTH;
            c.insets = new Insets(0, 15, 0, 15);
            this.add(levelLabel, c);
            
            c = new GridBagConstraints();
            c.gridx = 1;
            c.gridy = 3;
            c.weightx = 0.3;
            c.weighty = 0.05;
            c.anchor = GridBagConstraints.CENTER;
            c.fill = GridBagConstraints.BOTH;
            c.insets = new Insets(0, 15, 0, 15);
            this.add(seed,c) ;
            
                        c = new GridBagConstraints();
            c.gridx = 1;
            c.gridy = 4;
            c.weightx = 0.3;
            c.weighty = 0.05;
            c.anchor = GridBagConstraints.CENTER;
            c.fill = GridBagConstraints.BOTH;
            c.insets = new Insets(0, 15, 0, 15);
            this.add(ai,c) ;
            
                        c = new GridBagConstraints();
            c.gridx = 1;
            c.gridy = 5;
            c.weightx = 0.3;
            c.weighty = 0.05;
            c.anchor = GridBagConstraints.CENTER;
            c.fill = GridBagConstraints.BOTH;
            c.insets = new Insets(0, 15, 0, 15);
            this.add(fullSpeed,c) ;

            // Add generic button
            button.setBackground(Configuration.getColor("button", "#d4d0c8"));
            c = new GridBagConstraints();
            c.gridx = 1;
            c.gridy = 6;
            c.weightx = 0.3;
            c.weighty = 1.0;
            c.anchor = GridBagConstraints.NORTH;
            c.fill = GridBagConstraints.HORIZONTAL;
            c.insets = new Insets(15, 15, 15, 15);
            this.add(button, c);

            // Add event handling            
            enableEvents(KeyEvent.KEY_EVENT_MASK);
            this.addKeyListener(new KeyAdapter() {
                public void keyPressed(KeyEvent e) {
                    handleKeyEvent(e);
                }
            });
            
            button.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    handleButtonPressed();
                    component.requestFocus();
                }
            });
        }
        
        /**
         * Resizes all the static components, and invalidates the
         * current layout.
         */
        private void resizeComponents() {
            Dimension  size = scoreLabel.getSize();
            Font       font;
            int        unitSize;
            
            // Calculate the unit size
            size = board.getComponent().getSize();
            size.width /= board.getBoardWidth();
            size.height /= board.getBoardHeight();
            if (size.width > size.height) {
                unitSize = size.height;
            } else {
                unitSize = size.width;
            }

            // Adjust font sizes
            font = new Font("SansSerif", 
                            Font.BOLD, 
                            3 + (int) (unitSize / 1.8));
            scoreLabel.setFont(font);
            levelLabel.setFont(font);
            font = new Font("SansSerif", 
                            Font.PLAIN, 
                            2 + unitSize / 2);
            button.setFont(font);
            
            // Invalidate layout
            scoreLabel.invalidate();
            levelLabel.invalidate();
            button.invalidate();
        }
    }
}
